Guía maestra: Integración de Vimeo API con PHP + MySQL

Última revisión: 10 de agosto de 2025 — API base https://api.vimeo.com, OAuth 2.0, subidas TUS, enlaces de reproducción y controles de privacidad.

PHP 8.x MySQL 8.x TLS 1.2+ OAuth 2.0 TUS

Resumen ejecutivo

Vimeo expone una API REST (v3.x) bajo https://api.vimeo.com y usa OAuth 2.0 (tokens Bearer) para autenticación. Hay tres caminos frecuentes:

Subidas soportan tres enfoques: TUS (resumible), Pull (Vimeo descarga desde una URL), y Form/POST (legado). Recomendado: TUS. Además, los estados de upload/transcode se consultan por API (no hay webhook de “transcode completado” en la API estándar).

Mapa de servicios & capacidades

Requisitos previos

  1. Cuenta Vimeo y App creada en Developer (Client ID/Secret).
  2. Upload access aprobado para el scope upload si vas a subir por API.
  3. Generar Personal Access Token o implementar OAuth 2.0 (authorization code).
  4. Servidor PHP 8.x con curl, json y almacenamiento seguro de secretos.

Esquema MySQL base

-- Tokens (S2S con personal token o tokens OAuth por usuario)
CREATE TABLE vimeo_tokens (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  owner VARCHAR(80) NOT NULL,           -- 'APP' para token personal, o email/user_id
  access_token VARCHAR(500) NOT NULL,
  refresh_token VARCHAR(500),           -- sólo para OAuth authorization code
  scopes TEXT,
  expires_at DATETIME,                  -- NULL para personal tokens sin expiración
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  UNIQUE KEY uq_owner(owner)
);

-- Registro de videos
CREATE TABLE vimeo_videos (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  vimeo_uri VARCHAR(100),               -- p. ej., "/videos/123456789"
  vimeo_id BIGINT,                      -- 123456789
  name VARCHAR(255),
  status ENUM('creating','uploading','uploaded','transcoding','ready','error') NOT NULL,
  link TEXT,                            -- https://vimeo.com/123456789
  player_embed_url TEXT,                -- https://player.vimeo.com/video/123456789
  privacy_view VARCHAR(40),             -- anybody, unlisted, password, nobody, etc.
  last_check_at DATETIME,
  meta JSON,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL
);

-- Auditoría de llamadas (útil para troubleshooting y rate limits)
CREATE TABLE vimeo_calls (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  method VARCHAR(10),                   -- GET/POST/PATCH/DELETE
  endpoint VARCHAR(255),
  http_status INT,
  rate_limit INT,
  rate_remaining INT,
  rate_reset INT,
  req_body MEDIUMTEXT,
  resp_body MEDIUMTEXT,
  created_at DATETIME NOT NULL
);

Utilitarios PHP

<?php
// Variables de entorno recomendadas:
// VIMEO_CLIENT_ID, VIMEO_CLIENT_SECRET, VIMEO_PERSONAL_TOKEN (opcional)
$V_CLIENT_ID     = getenv('VIMEO_CLIENT_ID');
$V_CLIENT_SECRET = getenv('VIMEO_CLIENT_SECRET');
$V_PERSONAL      = getenv('VIMEO_PERSONAL_TOKEN'); // si usas token personal

function http_json($method, $url, $headers = [], $payload = null){
  $ch = curl_init($url);
  $h = array_merge(['Accept: application/json'], $headers);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_HTTPHEADER    => $h,
    CURLOPT_RETURNTRANSFER=> true,
    CURLOPT_TIMEOUT       => 120,
  ]);
  if ($payload !== null){
    $body = is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES);
    $h[] = 'Content-Type: application/json';
    curl_setopt($ch, CURLOPT_HTTPHEADER, $h);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  }
  $resp = curl_exec($ch);
  $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  $hdrs = curl_getinfo($ch);
  $err  = curl_error($ch);
  curl_close($ch);
  if ($resp === false){ throw new RuntimeException("cURL error: $err"); }
  return [$http, $resp, $hdrs];
}

function bearer($token){ return 'Authorization: Bearer '.$token; }

/** Client Credentials: sólo datos públicos */
function vimeo_token_client_credentials($scopes = 'public'){
  global $V_CLIENT_ID, $V_CLIENT_SECRET;
  $basic = base64_encode($V_CLIENT_ID.':'.$V_CLIENT_SECRET);
  $url   = 'https://api.vimeo.com/oauth/authorize/client';
  $payload = ['grant_type' => 'client_credentials', 'scope' => $scopes];
  list($http,$resp) = http_json('POST',$url,['Authorization: Basic '.$basic], $payload);
  if ($http >= 400) throw new RuntimeException("Token client_credentials HTTP $http: $resp");
  $data = json_decode($resp,true);
  return [$data['access_token'],$data['scope'] ?? ''];
}

/** Intercambio authorization code → access/refresh token */
function vimeo_token_authorization_code($code, $redirect_uri){
  global $V_CLIENT_ID, $V_CLIENT_SECRET;
  $basic = base64_encode($V_CLIENT_ID.':'.$V_CLIENT_SECRET);
  $url   = 'https://api.vimeo.com/oauth/access_token';
  $payload = [
    'grant_type'   => 'authorization_code',
    'code'         => $code,
    'redirect_uri' => $redirect_uri
  ];
  // Enviar como application/x-www-form-urlencoded:
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Basic '.$basic, 'Content-Type: application/x-www-form-urlencoded'],
    CURLOPT_POSTFIELDS     => http_build_query($payload),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 60
  ]);
  $resp = curl_exec($ch);
  $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);
  if ($http >= 400) throw new RuntimeException("Token authorization_code HTTP $http: $resp");
  return json_decode($resp,true); // access_token, refresh_token, scope, expires_in...
}
?>

Subida de video con TUS (resumible) – PHP

  1. Crear el recurso de video: POST /me/videos con upload.approach="tus" y upload.size (bytes). Respuesta incluye upload.upload_link (URL TUS) y uri del video.
  2. Enviar bytes a upload_link con método PATCH, cabeceras Tus-Resumable: 1.0.0, Upload-Offset, y cuerpo application/offset+octet-stream. Puedes subir en uno o varios chunks.
  3. Confirmar con HEAD upload_link verificando que Upload-Offset == tamaño.
  4. Monitorear por API: GET /videos/{id}?fields=uri,upload.status,transcode.status hasta que transcode.status="complete".
<?php
/**
 * Subida TUS simplificada (archivo único en memoria; para grandes, fragmenta en chunks).
 * Requiere token con scopes: upload, edit (y usualmente private/public).
 */
function vimeo_upload_tus($accessToken, $filePath, $name, $description = '', $privacyView = 'unlisted'){
  $size = filesize($filePath);
  if ($size === false) throw new RuntimeException('No se pudo leer tamaño de archivo');
  // 1) Crear video
  $payload = [
    'upload' => ['approach' => 'tus', 'size' => $size],
    'name'   => $name,
    'description' => $description,
    'privacy'=> ['view' => $privacyView]
  ];
  list($http, $resp) = http_json('POST','https://api.vimeo.com/me/videos', [bearer($accessToken)], $payload);
  if ($http >= 400) throw new RuntimeException("Crear video HTTP $http: $resp");
  $data = json_decode($resp,true);
  $uploadLink = $data['upload']['upload_link'] ?? null;
  $videoUri   = $data['uri'] ?? null; // p.ej. "/videos/123456789"
  if (!$uploadLink || !$videoUri) throw new RuntimeException('Respuesta sin upload_link/uri');

  // 2) PATCH TUS (subida en un solo envío)
  $fh = fopen($filePath, 'rb');
  $binary = stream_get_contents($fh);
  fclose($fh);

  $ch = curl_init($uploadLink);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => [
      'Tus-Resumable: 1.0.0',
      'Upload-Offset: 0',
      'Content-Type: application/offset+octet-stream',
      'Content-Length: '.$size
    ],
    CURLOPT_POSTFIELDS => $binary,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 0 // para subidas largas
  ]);
  $resp2 = curl_exec($ch);
  $http2 = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);
  if (!in_array($http2,[204,201,200])) throw new RuntimeException("TUS PATCH HTTP $http2: ".$resp2);

  // 3) HEAD para confirmar offset
  $ch = curl_init($uploadLink);
  curl_setopt_array($ch, [
    CURLOPT_NOBODY => true,
    CURLOPT_CUSTOMREQUEST => 'HEAD',
    CURLOPT_HTTPHEADER => ['Tus-Resumable: 1.0.0'],
    CURLOPT_RETURNTRANSFER => true
  ]);
  curl_exec($ch);
  $offset = 0;
  if (preg_match('/Upload-Offset:\s*(\d+)/i', implode("\n", headers_list()), $m)) {
    $offset = (int)$m[1];
  }
  curl_close($ch);

  // 4) Devuelve id y uri; monitorea transcode aparte
  $videoId = (int)basename($videoUri);
  return [$videoId, $videoUri];
}
?>

Notas: para archivos grandes, sube por chunks (mantén y actualiza Upload-Offset). Implementa reintentos idempotentes y verifica el offset antes de reanudar.

Subida por Pull (Vimeo descarga desde una URL)

<?php
$payload = [
  'upload' => ['approach' => 'pull', 'link' => 'https://mi-cdn.example.com/video.mp4'],
  'name'   => 'Demo Pull Upload',
  'privacy'=> ['view' => 'unlisted']
];
list($http,$resp) = http_json('POST','https://api.vimeo.com/me/videos',[bearer($V_PERSONAL)],$payload);
if ($http >= 400) { throw new RuntimeException("Pull upload HTTP $http: $resp"); }
$data = json_decode($resp,true);
$videoId = (int)basename($data['uri']);
?>

Monitoreo de upload/transcode

Consulta periódicamente el estado del video tras la subida. Campos clave: upload.status y transcode.status con valores in_progress, complete o error.

<?php
$fields = 'uri,link,player_embed_url,upload.status,transcode.status';
$url = "https://api.vimeo.com/videos/$videoId?fields=".rawurlencode($fields);
list($http,$resp) = http_json('GET', $url, [bearer($V_PERSONAL)]);
if ($http === 200) {
  $v = json_decode($resp,true);
  $statusUpload   = $v['upload']['status'] ?? '';
  $statusTrans    = $v['transcode']['status'] ?? '';
  $embedUrl       = $v['player_embed_url'] ?? null;
}
?>

Importante: la API estándar no emite webhook de “transcode completado”; debes polling hasta transcode.status="complete" antes de exponer el video al usuario.

Enlaces de reproducción (player y archivos)

Con el scope video_files puedes obtener URLs de archivos/streams (HLS/MP4) para reproductores externos; también dispones de link y player_embed_url para iframes.

<?php
$fields = 'link,player_embed_url,files';
$url = "https://api.vimeo.com/videos/$videoId?fields=".rawurlencode($fields);
list($http,$resp) = http_json('GET',$url,[bearer($V_PERSONAL)]);
$video = json_decode($resp,true);
$embed = $video['player_embed_url'] ?? null;   // iframe src
$files = $video['files'] ?? [];                // requiere scope video_files
?>

oEmbed (generar iframe desde una URL pública)

Si ya tienes la URL del video (https://vimeo.com/{id}), puedes generar HTML de embed vía oEmbed.

Privacidad y contraseñas

Ajusta la privacidad del video (p. ej. anybody, unlisted, password, nobody) y, si corresponde, define password. La API permite establecerla (no exponerla en lecturas públicas).

<?php
$payload = [
  'name' => 'Título actualizado',
  'privacy' => ['view' => 'password'],
  'password' => 'MiPassFuerte123'
];
list($http,$resp) = http_json('PATCH', "https://api.vimeo.com/videos/$videoId",
  [bearer($V_PERSONAL)], $payload);
if ($http >= 400) { /* manejar error */ }
?>

Paso a paso de implementación

Crea la app y define scopes

  • Genera Personal Access Token (si la app operará tu cuenta) con public, private, edit, upload (y video_files si necesitas URLs de archivos).
  • Si tu app actuará por usuarios, implementa OAuth 2.0 (authorization code) y almacena refresh token.
  • Si sólo lees datos públicos, usa client credentials (scope public).

Solicita Upload Access

  • Desde la página de tu App en Developer, pide aprobación para el scope upload (revisión manual).

Sube y monitorea

  • Usa TUS para robustez (reanudación por offset).
  • Tras subir, realiza polling de transcode.status hasta complete.

Entrega y privacidad

  • Decide privacy.view (unlisted/password/etc.) y restringe embed por dominio si corresponde.
  • Si necesitas archivos directos (HLS/MP4), añade el scope video_files.

Operación

  • Respeta límites (lee cabeceras de rate limit) y aplica backoff ante 429.
  • Audita respuestas y estados para soporte y métricas.

Rate limits y manejo de errores

SíntomaCausaAcción
HTTP 401Token inválido/expirado o token de app sin permisosRenueva token; usa token autenticado con scopes correctos.
HTTP 403Falta scope (p. ej. upload, edit) o falta aprobación de uploadAjusta scopes de la app; solicita upload access.
HTTP 429Límite de tasa superadoLee X-RateLimit-Remaining y X-RateLimit-Reset; aplica reintentos exponenciales.
Video sin miniatura/“no reproducible” tras subirTranscodificación en progresoHaz polling de transcode.status hasta complete antes de publicar.

Seguridad & buenas prácticas

Anexos & referencias

Consulta siempre la documentación vigente de Vimeo y tu plan de cuenta; ciertos endpoints/características requieren planes superiores o aprobaciones adicionales.